package com.example.filter;

import com.example.entity.RestBean;
import com.example.utils.Const;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 流量限制过滤器
 * 用于限制同一IP地址的请求频率，防止服务被恶意访问
 * 作者:xuSen
 * 日期2025/4/19 19:24
 */
@Component
@Order(Const.ORDER_LIMIT)
public class FlowLimitFilter extends HttpFilter {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 过滤器的主要逻辑方法
     * 检查请求者的IP地址是否达到访问频率限制，如果没有达到则继续执行请求链
     *
     * @param request 请求对象，包含请求的所有信息
     * @param response 响应对象，用于向客户端发送响应
     * @param chain 过滤器链，用于将请求传递给下一个过滤器或最终的目标资源
     * @throws IOException 如果在执行过程中发生I/O错误
     * @throws ServletException 如果在执行过程中发生Servlet错误
     */
    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String address = request.getRemoteAddr();
        if (this.tryCount(address)) chain.doFilter(request, response);
        else {
            this.writeBlockMessage(response);
        }
    }

    /**
     * 写入访问受限的消息到响应中
     * 当请求被限制时调用此方法，向客户端返回403状态码和受限的消息
     *
     * @param response 响应对象，用于向客户端发送响应
     */
    private void writeBlockMessage(HttpServletResponse response) {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=UTF-8");
        try {
            response.getWriter().write(RestBean.forbidden("访问过于频繁，请稍后再试").asJsonString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 尝试增加IP的访问计数并检查是否达到限制
     * 此方法同步执行以防止并发问题，确保同一时间只有一个线程可以修改计数
     *
     * @param ip 请求者的IP地址，用于标识和限制访问
     * @return 如果访问未受限则返回true，否则返回false
     */
    private boolean tryCount(String ip) {
        synchronized (ip.intern()) {
            if (stringRedisTemplate.hasKey(Const.FLOW_LIMIT_BLOCK + ip)) {
                return false;
            }
            return this.limitPeriodCheck(ip);
        }
    }

    /**
     * 检查IP地址在限制周期内的访问次数是否达到限制
     * 如果访问次数超过限制，则在一段时间内禁止该IP的访问
     *
     * @param ip 请求者的IP地址，用于标识和限制访问
     * @return 如果访问未受限则返回true，否则返回false
     */
    private boolean limitPeriodCheck(String ip){
        if(stringRedisTemplate.hasKey(Const.FLOW_LIMIT_COUNTER + ip)){
            Long increment = Optional.ofNullable(stringRedisTemplate.opsForValue().increment(Const.FLOW_LIMIT_COUNTER + ip)).orElse(0L) ;
            if(increment>=10){
                stringRedisTemplate.opsForValue().set(Const.FLOW_LIMIT_BLOCK + ip," ",30, TimeUnit.SECONDS);
                return false;
            }
        }else{
            stringRedisTemplate.opsForValue().set(Const.FLOW_LIMIT_COUNTER + ip,"1",3, TimeUnit.SECONDS);
        }
        return true;
    }
}
